﻿// Arduino entry
#include <Arduino.h>
#include <HardwareSerial.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#include <esp_system.h>
#include <math.h>

#include "gps.h"
#include "aprs.h"
#include "config.h"
#include "oled.h"

// ----- GPS runtime moved to gps.* -----

// ===== IO mapping (confirmed) =====
// GPS (M210Z): RX1=GPIO7 (鈫怗PS TX), TX1=GPIO6 (鈫扜PS RX), ONOFF=GPIO5
// Radio audio: SPK_IN=GPIO4 (ADC), MIC_OUT=GPIO0 (AFSK PWM), PTT=GPIO2(+LED), SQL=GPIO10
// OLED I2C   : SDA=GPIO1, SCL=GPIO3
// User/LED   : KEY=GPIO9, GPS_LED=GPIO8
// Debug UART : U0RXD=GPIO20, U0TXD=GPIO21
// USB native : D-=GPIO18, D+=GPIO19

static const int PIN_SPK_IN   = 4;   // ADC input from radio speaker
static const int PIN_MIC_OUT  = 0;   // PWM/AFSK output to radio MIC
static const int PIN_PTT      = 2;   // PTT control
static const int PIN_SQL      = 10;  // Squelch detect
static const int PIN_KEY      = 9;   // User button
static const int PIN_LED_WIFI = -1;  // no dedicated WiFi LED
static const int PIN_LED_GPS  = 8;   // GPS status LED
static const int PIN_DBG_RX   = 20;  // Debug UART RX (U0RXD)
static const int PIN_DBG_TX   = 21;  // Debug UART TX (U0TXD)
static const int PIN_GPS_ONOFF= 5;   // GPS power/enable control

// 馃摗 GPS 涓插彛瀹氫箟锛圡210Z锛?
// GPS is managed by gps.* module
#define GPS_RX 7  // GPIO7 鈫?GPS TXD
#define GPS_TX 6  // GPIO6 鈫?GPS RXD


// gpsSetBaudByIndex moved to gps.*

// OLED 寮曡剼瀹氫箟锛圛2C锛?
#define OLED_SCL 3
#define OLED_SDA 1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
// OLED 鏄剧ず瀵硅薄宸茶縼绉诲埌 oled.cpp 妯″潡涓粺涓€绠＄悊

// 馃寪 WiFi 閰嶇疆
const char* ssid = "BD3ND_J";
const char* password = "12345678abc";

// 馃寪 APRS 涓婁紶鏈嶅姟鍣ㄤ俊鎭?
const char* callsign = "BD3ND-10";  // 鏇挎崲涓轰綘鐨勫懠鍙?
const char* aprsServer = "rotate.aprs.net";
const uint16_t aprsPort = 14580;
const char symTable = '/';   // 浣跨敤绗﹀彿琛?A
const char symChar  = '>';   // "杞? 鍥炬爣

WiFiClient client;
uint16_t passcodeNum = 0;
unsigned long lastSend = 0;
const unsigned long sendInterval = 5UL * 60UL * 1000UL; // 5 鍒嗛挓闂撮殧
unsigned long lastDebug = 0;

// ===== Persistent config via NVS; moved to config.* =====

// ========= OLED UI锛堟洿澶у瓧鍙?闂磋窛 + 澶氶〉锛?=========
// 璇存槑锛?// - 浣跨敤 Adafruit_SSD1306 缁忓吀瀛椾綋锛岄€氳繃 setTextSize 鏀惧ぇ瀛楀彿锛?// - 鑷畾涔夎闂磋窛锛屾彁鍗囧彲璇绘€э紱
// - 鏀寔澶氶〉鏄剧ず锛岃嚜鍔ㄨ疆鎾紝骞跺彲鎸夐敭鎵嬪姩鍒囨崲锛?// - 鍦ㄥ惎鍔ㄩ樁娈碉紝鍒嗘楠よ緭鍑哄垵濮嬪寲杩涘害銆?
// UI 椤垫帶鍒?static uint8_t uiPage = 0;                     // 褰撳墠椤电储寮?static const uint8_t UI_PAGE_COUNT = 3;        // 鎬婚〉鏁帮紙鍙寜闇€璋冩暣锛?static unsigned long uiLastSwitch = 0;         // 涓婃缈婚〉鏃堕棿鎴?static const unsigned long uiSwitchInterval = 5000; // 鑷姩缈婚〉鏃堕棿锛堟绉掞級
static unsigned long uiLastDraw = 0;           // 涓婃缁樺埗鏃堕棿鎴?static const unsigned long uiDrawInterval = 400;    // 缁樺埗鍒锋柊闂撮殧锛堟绉掞級

// 鎸夐敭娑堟姈锛岀敤浜庢墜鍔ㄥ垏鎹㈤〉
static int keyPrev = HIGH;
static unsigned long keyLastChange = 0;
static const unsigned long keyDebounceMs = 60;

// 鈥斺€?甯哥敤灏忓伐鍏?鈥斺€?// 鏍规嵁瀛楀彿鎵撳嵃鍗曡鏂囨湰锛屽苟鎸夎瀹氳璺濋€掑 y 鍧愭爣锛堟柟渚挎帓鐗堬級
static void oledPrintLine(const String &text, uint8_t textSize, int16_t x, int16_t &y, int16_t lineGap = 2) {
  display.setTextSize(textSize);
  display.setCursor(x, y);
  display.print(text);
  // 缁忓吀瀛椾綋琛岄珮锛? 鍍忕礌锛涗箻浠ュ瓧鍙峰啀鍔犺嚜瀹氫箟闂磋窛
  y += (int16_t)(8 * textSize) + lineGap;
}

// 鍚姩闃舵鍗曟鍙嶉锛堟爣棰?杩涘害鏉★級
static void oledShowStartupStep(const __FlashStringHelper *title, uint8_t step, uint8_t total) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  int16_t y = 0;
  oledPrintLine(F("APRS ESP32C3"), 2, 0, y, 4);        // 鏍囬锛氭洿澶у瓧鍙?  oledPrintLine(title, 1, 0, y, 4);                     // 褰撳墠姝ラ

  // 绠€鍗曡繘搴︽潯锛堝簳閮級
  const int barX = 0, barY = 54, barW = 128, barH = 10;
  display.drawRect(barX, barY, barW, barH, SSD1306_WHITE);
  int fillW = (int)((step * (barW - 2)) / max(1, (int)total));
  display.fillRect(barX + 1, barY + 1, fillW, barH - 2, SSD1306_WHITE);

  display.display();
}

// 椤?0锛氳澶?缃戠粶/鍛煎彿绠€瑕?static void uiDrawPage0() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  int16_t y = 0;
  oledPrintLine(String(F("鍛煎彿:")) + cfg.callsign, 2, 0, y, 6);
  String ip = (WiFi.status() == WL_CONNECTED) ? WiFi.localIP().toString() : String(F("鏈繛鎺?));
  oledPrintLine(String(F("WiFi:")) + (cfg.wifiSsid.length()?cfg.wifiSsid:"<N/A>"), 1, 0, y, 3);
  oledPrintLine(String(F("IP  :")) + ip, 1, 0, y, 3);
  oledPrintLine(String(F("APRS:")) + cfg.aprsServer + ":" + String(cfg.aprsPort), 1, 0, y, 3);
  display.display();
}

// 椤?1锛氬畾浣嶄俊鎭紙鏇村ぇ瀛楀彿锛?static void uiDrawPage1() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  int16_t y = 0;
  bool fix = gpsHasFix();
  oledPrintLine(String(F("瀹氫綅:")) + (fix?F("鏈夋晥"):F("鏃犳晥")), 1, 0, y, 4);
  // 鍔犲ぇ鍧愭爣瀛楀彿锛屾彁楂樺彲璇绘€?  oledPrintLine(String(F("Lat:")) + String(gpsLatitude(), 5), 2, 0, y, 4);
  oledPrintLine(String(F("Lon:")) + String(gpsLongitude(), 5), 2, 0, y, 4);
  oledPrintLine(String(F("UTC:")) + gpsLatestTime(), 1, 0, y, 2);
  display.display();
}

// 椤?2锛氬皠棰戜笌绯荤粺锛圫QL/PTT/閲嶇疆鍘熷洜绠€瑕侊級
static void uiDrawPage2() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  int16_t y = 0;

  // SQL/PTT 鐘舵€侊紙渚濇嵁纭欢鐢靛钩锛岀畝鍗曞睍绀猴級
  int sql = digitalRead(PIN_SQL);
  int ptt = digitalRead(PIN_PTT);
  oledPrintLine(F("灏勯鐘舵€?), 2, 0, y, 6);
  oledPrintLine(String(F("SQL:")) + (sql?F("寮€"):F("鍏?)), 1, 0, y, 3);
  oledPrintLine(String(F("PTT:")) + (ptt?F("楂?):F("浣?)), 1, 0, y, 3);

  // 閲嶅惎鍘熷洜锛堢畝瑕侊級
  esp_reset_reason_t r = esp_reset_reason();
  const char* rs = "UNKNOWN";
  switch (r) {
    case ESP_RST_POWERON: rs = "POWERON"; break;
    case ESP_RST_SW:      rs = "SW"; break;
    case ESP_RST_PANIC:   rs = "PANIC"; break;
    case ESP_RST_INT_WDT: rs = "INT_WDT"; break;
    case ESP_RST_TASK_WDT:rs = "TASK_WDT"; break;
    case ESP_RST_WDT:     rs = "WDT"; break;
    case ESP_RST_DEEPSLEEP:rs = "DEEPSLEEP"; break;
    case ESP_RST_BROWNOUT:rs = "BROWNOUT"; break;
    case ESP_RST_SDIO:    rs = "SDIO"; break;
    default: break;
  }
  oledPrintLine(String(F("澶嶄綅:")) + rs, 1, 0, y, 3);
  display.display();
}

// 鏍规嵁褰撳墠椤电粯鍒?static void uiDrawCurrentPage() {
  switch (uiPage % UI_PAGE_COUNT) {
    case 0: uiDrawPage0(); break;
    case 1: uiDrawPage1(); break;
    case 2: uiDrawPage2(); break;
    default: uiDrawPage0(); break;
  }
}

// UI 鏇存柊锛堝鐞嗘寜閿垏椤典笌鑷姩杞挱 + 瀹氭湡鍒锋柊锛?static void uiUpdate() {
  // 鎵嬪姩鍒囬〉锛堟寜閿寜涓嬫椂缈婚〉锛屾秷鎶栵級
  int ks = digitalRead(PIN_KEY);
  unsigned long now = millis();
  if (ks != keyPrev) {
    if (now - keyLastChange > keyDebounceMs) {
      keyLastChange = now;
      keyPrev = ks;
      if (ks == LOW) { // 鎸変笅瑙﹀彂
        uiPage = (uiPage + 1) % UI_PAGE_COUNT;
        uiLastSwitch = now; // 閲嶇疆鑷姩杞挱璁℃椂
        uiDrawCurrentPage();
      }
    }
  }

  // 鑷姩杞挱
  if (now - uiLastSwitch >= uiSwitchInterval) {
    uiLastSwitch = now;
    uiPage = (uiPage + 1) % UI_PAGE_COUNT;
    uiDrawCurrentPage();
  }

  // 瀹氭湡閲嶇粯锛堜緥濡傚姩鎬佹暟鍊煎彉鍖栵級
  if (now - uiLastDraw >= uiDrawInterval) {
    uiLastDraw = now;
    uiDrawCurrentPage();
  }
}

// 馃洶锔?瀹炴椂瀹氫綅鏁版嵁
// GPS live data now provided via gpsLatitude()/gpsLongitude()/gpsLatestTime()

// Forward decls
void handleUsbCli();
void connectToWiFiCfg();
// GPS/APRS helpers moved into modules

// GPS ON/OFF helpers provided by gps.*

// Print last reset reason on boot
void printResetInfo() {
  esp_reset_reason_t r = esp_reset_reason();
  const char* rs = "UNKNOWN";
  switch (r) {
    case ESP_RST_POWERON: rs = "POWERON"; break;
    case ESP_RST_SW:      rs = "SW"; break;
    case ESP_RST_PANIC:   rs = "PANIC"; break;
    case ESP_RST_INT_WDT: rs = "INT_WDT"; break;
    case ESP_RST_TASK_WDT:rs = "TASK_WDT"; break;
    case ESP_RST_WDT:     rs = "WDT"; break;
    case ESP_RST_DEEPSLEEP:rs = "DEEPSLEEP"; break;
    case ESP_RST_BROWNOUT:rs = "BROWNOUT"; break;
    case ESP_RST_SDIO:    rs = "SDIO"; break;
    default: break;
  }
  Serial.printf("[ResetReason] %s (%d)\n", rs, (int)r);
}

void connectToWiFi() {
  Serial.print("[WiFi] 杩炴帴鍒?WiFi 缃戠粶锛?); Serial.println(ssid);
  WiFi.begin(ssid, password);
  int retry = 0;
  while (WiFi.status() != WL_CONNECTED && retry < 20) {
    delay(500);
    Serial.print(".");
    retry++;
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\n[WiFi] 鎴愬姛杩炴帴锛孖P鍦板潃: " + WiFi.localIP().toString());
  } else {
    Serial.println("\n[WiFi] 杩炴帴澶辫触锛岃妫€鏌SID涓庡瘑鐮?);
  }
}

// 鈥斺€?APRS-IS Passcode 璁＄畻 鈥斺€?
uint16_t calculatePasscode(const char* call) {
  String root = String(call);
  int dash = root.indexOf('-');
  if (dash > 0) root = root.substring(0, dash);
  root.toUpperCase();
  uint8_t hi = 0x73, lo = 0xE2;
  size_t len = root.length();
  for (size_t i = 0; i < len; i += 2) {
    uint8_t c1 = root.charAt(i);
    uint8_t c2 = (i + 1 < len) ? root.charAt(i + 1) : 0;
    hi ^= c1;
    lo ^= c2;
  }
  return (uint16_t)(((hi & 0x7F) << 8) | lo);
}

// config helpers moved to config.*

void handleUsbCli() {
  static String line;
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\r') continue;
    if (c == '\n') {
      line.trim();
      if (line.length() == 0) { line = ""; continue; }
      if (line.equalsIgnoreCase("help")) {
        Serial.println("Commands: show | save | set CALLSIGN <str> | set WIFI <ssid> <pass> | set APRS <host> <port> | set SYMBOL </|\\> <char> | reboot");
      } else if (line.equalsIgnoreCase("show")) {
        printConfig();
      } else if (line.startsWith("set ")) {
        int sp1 = line.indexOf(' ');
        int sp2 = line.indexOf(' ', sp1 + 1);
        String key = line.substring(sp1 + 1, sp2);
        if (key.equalsIgnoreCase("CALLSIGN")) {
          String v = line.substring(sp2 + 1);
          v.trim(); if (v.length() > 0) cfg.callsign = v;
          Serial.println("[閰嶇疆] callsign 宸叉洿鏂?);
        } else if (key.equalsIgnoreCase("WIFI")) {
          int sp3 = line.indexOf(' ', sp2 + 1);
          if (sp3 > 0) {
            cfg.wifiSsid = line.substring(sp2 + 1, sp3);
            cfg.wifiPass = line.substring(sp3 + 1);
            Serial.println("[閰嶇疆] WiFi 宸叉洿鏂?);
          } else {
            Serial.println("鐢ㄦ硶: set WIFI <ssid> <pass>");
          }
        } else if (key.equalsIgnoreCase("APRS")) {
          int sp3 = line.indexOf(' ', sp2 + 1);
          if (sp3 > 0) {
            cfg.aprsServer = line.substring(sp2 + 1, sp3);
            cfg.aprsPort = (uint16_t)line.substring(sp3 + 1).toInt();
            Serial.println("[閰嶇疆] APRS-IS 宸叉洿鏂?);
          } else {
            Serial.println("鐢ㄦ硶: set APRS <host> <port>");
          }
        } else if (key.equalsIgnoreCase("SYMBOL")) {
          int sp3 = line.indexOf(' ', sp2 + 1);
          if (sp3 > 0 && (line[sp2 + 1] == '/' || line[sp2 + 1] == '\\')) {
            cfg.symTable = line[sp2 + 1];
            String cstr = line.substring(sp3 + 1); cstr.trim();
            if (cstr.length() > 0) cfg.symChar = cstr[0];
            Serial.println("[閰嶇疆] 绗﹀彿宸叉洿鏂?);
          } else {
            Serial.println("鐢ㄦ硶: set SYMBOL </|\\> <char>");
          }
        } else {
          Serial.println("鏈煡 set 閫夐」");
        }
      } else if (line.startsWith("gps ")) {
        int sp1 = line.indexOf(' ');
        int sp2 = line.indexOf(' ', sp1 + 1);
        String sub = (sp2>0) ? line.substring(sp1+1, sp2) : line.substring(sp1+1);
        sub.trim(); sub.toLowerCase();
        if (sub == "debug") {
          String arg = (sp2>0) ? line.substring(sp2+1) : ""; arg.trim(); arg.toLowerCase();
          bool on = !(arg == "off" || arg == "0");
          gpsSetDebug(on);
          Serial.printf("[GPS] debug=%s\n", on?"on":"off");
        } else if (sub == "raw") {
          String arg = (sp2>0) ? line.substring(sp2+1) : ""; arg.trim(); arg.toLowerCase();
          bool on = !(arg == "off" || arg == "0");
          gpsSetRaw(on);
          Serial.printf("[GPS] raw echo=%s (warn: very verbose)\n", on?"on":"off");
        } else if (sub == "on") {
          gpsPowerOn(); Serial.println("[GPS] ONOFF=HIGH (enabled)");
        } else if (sub == "off") {
          gpsPowerOff(); Serial.println("[GPS] ONOFF=LOW (disabled)");
        } else if (sub == "reset") {
          Serial.println("[GPS] reset pulse...");
          gpsPowerOff(); delay(200); gpsPowerOn();
        } else if (sub == "send") {
          // Send raw string to GPS (append CRLF if not present)
          String payload = (sp2>0) ? line.substring(sp2+1) : "";
          if (payload.length()==0) { Serial.println("鐢ㄦ硶: gps send <text>"); }
          else { gpsSendRaw(payload); Serial.print("[GPS] sent raw: "); Serial.print(payload); }
        } else if (sub == "nmea") {
          // Build NMEA with checksum automatically from payload (without '$')
          String payload = (sp2>0) ? line.substring(sp2+1) : ""; payload.trim();
          if (payload.length()==0) { Serial.println("鐢ㄦ硶: gps nmea <payload-no-$>"); }
          else { gpsSendNmea(payload); Serial.print("[GPS] sent NMEA payload: "); Serial.println(payload); }
        } else if (sub == "hex") {
          // Send hex bytes to GPS, e.g. "gps hex B5 62 06 08 00 00 0E 30"
          String rest = (sp2>0) ? line.substring(sp2+1) : ""; rest.trim();
          if (rest.length()==0) { Serial.println("鐢ㄦ硶: gps hex <byte1> <byte2> ... (鍗佸叚杩涘埗)"); }
          else { gpsSendHexStr(rest); Serial.println("[GPS] sent HEX sequence"); }
        } else if (sub == "baud") {
          String arg = (sp2>0) ? line.substring(sp2+1) : ""; arg.trim();
          int br = arg.toInt(); if (br<=0) br = 9600;
          gpsSetBaud(br);
        } else if (sub == "scan") {
          gpsScanBaud();
        } else {
          Serial.println("gps commands: gps debug on|off | gps raw on|off | gps on|off|reset | gps baud <rate> | gps scan | gps nmea <payload> | gps send <text> | gps hex <b1> <b2> ...");
        }
      } else if (line.equalsIgnoreCase("save")) {
        saveConfig();
        Serial.println("[閰嶇疆] 宸蹭繚瀛樺埌 NVS");
      } else if (line.equalsIgnoreCase("reboot")) {
        Serial.println("[绯荤粺] 閲嶅惎...");
        delay(100);
        ESP.restart();
      } else {
        Serial.println("[鎻愮ず] 杈撳叆 'help' 鏌ョ湅鍛戒护");
      }
      line = "";
    } else {
      line += c;
    }
  }
}

// 鈥斺€?鍧愭爣鏍煎紡鍖栧嚱鏁?鈥斺€?
String aprsLat(float lat) { return aprs_formatLat(lat); }
String aprsLon(float lon) { return aprs_formatLon(lon); }

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("[System] Booting");
  printResetInfo();
  Serial.println("[绯荤粺] 鍒濆鍖栦覆鍙?);

  // IO init
  pinMode(PIN_PTT, OUTPUT); digitalWrite(PIN_PTT, LOW);
  pinMode(PIN_SQL, INPUT);
  pinMode(PIN_KEY, INPUT_PULLUP);
  if (PIN_LED_WIFI >= 0) { pinMode(PIN_LED_WIFI, OUTPUT); digitalWrite(PIN_LED_WIFI, LOW); }
  pinMode(PIN_LED_GPS, OUTPUT);  digitalWrite(PIN_LED_GPS, LOW);
  // Keep GPS enabled by default (active-high ON)
  gpsPowerOn();
  Serial.println("[GPS] ONOFF pin set HIGH (active)");

  // Load persistent config
  loadConfig();
  if (cfg.callsign == "NOCALL-10") {
    Serial.println("[鎻愮ず] 棣栨浣跨敤锛屽缓璁€氳繃 USB 杩涜閰嶇疆 (杈撳叆 'help')");
  }

  // M210Z 榛樿涓插彛娉㈢壒鐜?115200锛屽浐瀹氫笉鎵弿
  gpsInit(GPS_RX, GPS_TX, PIN_GPS_ONOFF, PIN_LED_GPS, true, 115200);
  // delay after gps init
  Serial.println("[绯荤粺] GPS 涓插彛宸插紑鍚?);

  Wire.begin(OLED_SDA, OLED_SCL);
  Wire.setClock(400000);  // 璁剧疆 I2C 閫熺巼涓?400kHz
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("[閿欒] OLED 鍒濆鍖栧け璐ワ紒"));
  } else {
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.display();
  }

  // 鈥斺€?鍚姩闃舵锛氱畝鍗曡緭鍑烘楠ゅ埌 OLED 鈥斺€?  oledShowStartupStep(F("鍔犺浇閰嶇疆..."), 1, 5);
  delay(150);
  oledShowStartupStep(F("鍒濆鍖?GPS..."), 2, 5);
  delay(150);
  oledShowStartupStep(F("杩炴帴 WiFi..."), 3, 5);
  delay(150);

  passcodeNum = aprs_calculatePasscode(cfg.callsign.c_str());
  if (digitalRead(PIN_KEY) == LOW) {
    Serial.println("[SafeMode] KEY held: skip WiFi");
  } else {
    connectToWiFiCfg();
  }
  // 缁х画杈撳嚭鍚姩闃舵姝ラ骞跺睍绀洪椤?  oledShowStartupStep(F("鍑嗗 APRS..."), 4, 5);
  delay(150);
  oledShowStartupStep(F("鍚姩瀹屾垚"), 5, 5);
  delay(400);
  uiDrawCurrentPage();
}

void loop() {
  gpsHandle();
  if (millis() - lastSend >= sendInterval && gpsHasFix()) {
    if (aprs_sendPosition(client, cfg.callsign.c_str(), cfg.aprsServer.c_str(), cfg.aprsPort, cfg.symTable, cfg.symChar, passcodeNum, gpsLatitude(), gpsLongitude()))
      lastSend = millis();
  }
  handleUsbCli();
  if (millis() - lastDebug > 10000) {
    lastDebug = millis();
    Serial.printf("[鐘舵€乚 瀹氫綅鏈夋晥: %.5f, %.5f\n", gpsLatitude(), gpsLongitude());
  }
  // OLED UI 鍒锋柊锛堣嚜鍔ㄨ疆鎾?+ 鎵嬪姩鍒囨崲锛?  uiUpdate();
}

// 浣跨敤 NVS 閰嶇疆鐨?WiFi 杩炴帴
void connectToWiFiCfg() {
  Serial.print("[WiFi] Connecting: "); Serial.println(cfg.wifiSsid);
  WiFi.persistent(false);
  WiFi.disconnect(true, true);
  delay(100);
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(true);
  // lower TX power to reduce inrush / supply dip
  WiFi.setTxPower(WIFI_POWER_8_5dBm);
  delay(200);

  WiFi.begin(cfg.wifiSsid.c_str(), cfg.wifiPass.c_str());
  int retry = 0;
  while (WiFi.status() != WL_CONNECTED && retry < 40) {
    delay(250);
    Serial.print(".");
    retry++;
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\n[WiFi] Connected: " + WiFi.localIP().toString());
    if (PIN_LED_WIFI >= 0) digitalWrite(PIN_LED_WIFI, HIGH);
  } else {
    Serial.println("\n[WiFi] Connect failed");
    if (PIN_LED_WIFI >= 0) digitalWrite(PIN_LED_WIFI, LOW);
  }
}

#if 0
void parseRMC(String rmc) {
  int idx = 0;
  String parts[20];
  while (rmc.length() > 0 && idx < 20) {
    int comma = rmc.indexOf(',');
    if (comma == -1) {
      parts[idx++] = rmc;
      break;
    } else {
      parts[idx++] = rmc.substring(0, comma);
      rmc = rmc.substring(comma + 1);
    }
  }

  if (parts[2] == "A") {
    latestTime = parts[1];
    float rawLat = parts[3].toFloat();
    float rawLon = parts[5].toFloat();
    int degLat = int(rawLat / 100);
    int degLon = int(rawLon / 100);
    float minLat = rawLat - degLat * 100;
    float minLon = rawLon - degLon * 100;
    latitude = degLat + minLat / 60.0;
    longitude = degLon + minLon / 60.0;
    if (parts[4] == "S") latitude = -latitude;
    if (parts[6] == "W") longitude = -longitude;
    if (PIN_LED_GPS >= 0) digitalWrite(PIN_LED_GPS, HIGH);
    if (gpsDebug) Serial.printf("[GPS] FIX ok: lat=%.5f lon=%.5f time=%s\n", latitude, longitude, latestTime.c_str());
  }
  else {
    if (PIN_LED_GPS >= 0) digitalWrite(PIN_LED_GPS, LOW);
    if (gpsDebug) Serial.println("[GPS] FIX invalid (RMC status != A)");
  }
}

// Build an NMEA sentence with checksum and CRLF from payload without '$'
String nmeaBuild(const String &payloadNoDollar) {
  String pl = payloadNoDollar;
  String inner = (pl.length() && pl[0] == '$') ? pl.substring(1) : pl;
  uint8_t cs = 0; for (size_t i = 0; i < inner.length(); ++i) cs ^= (uint8_t)inner[i];
  char hex[3]; snprintf(hex, sizeof(hex), "%02X", cs);
  return String("$") + inner + "*" + hex + "\r\n";
}

bool parseHexByteToken(const String &tok, uint8_t &out) {
  auto hexv = [](char c)->int { if(c>='0'&&c<='9') return c-'0'; if(c>='A'&&c<='F') return 10+c-'A'; if(c>='a'&&c<='f') return 10+c-'a'; return -1; };
  if (tok.length() < 1 || tok.length() > 2) return false;
  int v = 0; for (size_t i=0;i<tok.length();++i){int n=hexv(tok[i]); if(n<0) return false; v = (v<<4)|n;}
  out = (uint8_t)v; return true;
}

// $..GGA,time,lat,N,lon,E,fix,numsats,hdop,alt,M,geoid,M,age,ref*CS
void parseGGA(String gga) {
  if (!nmeaChecksumOk(gga)) { if (gpsDebug) Serial.println("[GPS/GGA] checksum BAD"); return; }
  String s = gga; int idx = 0; String p[20];
  while (s.length() && idx < 20) {
    int c = s.indexOf(',');
    if (c < 0) { p[idx++] = s; break; }
    p[idx++] = s.substring(0, c); s = s.substring(c+1);
  }
  auto toLat = [](const String &v, const String &hemi){
    float raw = v.toFloat(); int d = int(raw/100); float m = raw - d*100; float out = d + m/60.0f; if (hemi == "S") out = -out; return out; };
  auto toLon = [](const String &v, const String &hemi){
    float raw = v.toFloat(); int d = int(raw/100); float m = raw - d*100; float out = d + m/60.0f; if (hemi == "W") out = -out; return out; };
  int fixQ = p[6].toInt();
  int sats = p[7].toInt();
  float hdop = p[8].toFloat();
  float alt  = p[9].toFloat();
  float plat = toLat(p[2], p[3]);
  float plon = toLon(p[4], p[5]);
  const char* qdesc = (fixQ==0?"invalid":fixQ==1?"GPS":fixQ==2?"DGPS":fixQ==3?"PPS":fixQ==4?"RTK-FIX":fixQ==5?"RTK-FLOAT":fixQ==6?"DEAD-RECKON":fixQ==7?"MANUAL":fixQ==8?"SIM":"?");
  if (fixQ>0) { latitude = plat; longitude = plon; }
  if (gpsDebug) {
    Serial.printf("[GPS/GGA] fix=%d(%s) sats=%d hdop=%.1f alt=%.1fm lat=%.5f lon=%.5f\n",
                  fixQ, qdesc, sats, hdop, alt, plat, plon);
  }
}

// $..GSA,opMode,fixType,sv1..sv12,PDOP,HDOP,VDOP*CS
void parseGSA(String gsa) {
  if (!nmeaChecksumOk(gsa)) { if (gpsDebug) Serial.println("[GPS/GSA] checksum BAD"); return; }
  String s = gsa; int idx = 0; String p[22];
  while (s.length() && idx < 22) { int c = s.indexOf(','); if (c<0){p[idx++]=s; break;} p[idx++]=s.substring(0,c); s=s.substring(c+1);}  
  int fixType = p[2].toInt();
  float pdop = p[15].toFloat();
  float hdop = p[16].toFloat();
  float vdop = p[17].toFloat();
  const char* fdesc = (fixType==1?"no-fix":fixType==2?"2D":fixType==3?"3D":"?");
  if (gpsDebug) {
    Serial.printf("[GPS/GSA] fix=%d(%s) PDOP=%.1f HDOP=%.1f VDOP=%.1f\n", fixType, fdesc, pdop, hdop, vdop);
  }
}

// $..GSV,totalMsgs,msgNum,totalSats, (satPRN,el,az,CN0)xN *CS
void parseGSV(String gsv) {
  if (!nmeaChecksumOk(gsv)) { if (gpsDebug) Serial.println("[GPS/GSV] checksum BAD"); return; }
  String s = gsv; int idx = 0; String p[32];
  while (s.length() && idx < 32) { int c = s.indexOf(','); if (c<0){p[idx++]=s; break;} p[idx++]=s.substring(0,c); s=s.substring(c+1);}  
  int totalMsgs = p[1].toInt();
  int msgNum    = p[2].toInt();
  int totalSats = p[3].toInt();
  if (gpsDebug) {
    Serial.printf("[GPS/GSV] msg %d/%d, total sats=%d\n", msgNum, totalMsgs, totalSats);
  }
}

// $..VTG,cogT,T,cogM,M,knots,N,kmh,K*CS
void parseVTG(String vtg) {
  if (!nmeaChecksumOk(vtg)) { if (gpsDebug) Serial.println("[GPS/VTG] checksum BAD"); return; }
  String s = vtg; int idx = 0; String p[16];
  while (s.length() && idx < 16) { int c = s.indexOf(','); if (c<0){p[idx++]=s; break;} p[idx++]=s.substring(0,c); s=s.substring(c+1);}  
  float cog = p[1].toFloat();
  float spd = p[7].toFloat(); // km/h
  if (gpsDebug) {
    Serial.printf("[GPS/VTG] course=%.1f deg, speed=%.1f km/h\n", cog, spd);
  }
}

void sendAprsPosition() {
  if (!client.connected()) {
    if (!client.connect(cfg.aprsServer.c_str(), cfg.aprsPort)) {
      Serial.println("鉂?杩炴帴 APRS-IS 澶辫触");
      return;
    }
    client.printf("user %s pass %u vers ESP32-APRS 1.0\r\n", cfg.callsign.c_str(), passcodeNum);
    Serial.printf("鉁?鐧诲綍 APRS-IS 鎴愬姛 (passcode: %u)\n", passcodeNum);
  }
  String packet = String(cfg.callsign) + ">APRS,TCPIP*:=" + aprsLat(latitude) + cfg.symTable + aprsLon(longitude) + cfg.symChar + " ESP32 APRS";
  client.println(packet);
  Serial.println("鉁?宸插彂閫? " + packet);
  Serial.println("[璋冭瘯] 涓婁紶 APRS 鎶ユ枃鍐呭锛? + packet);
}
#endif





